Skip to content

test: stop CLI-spawn timeouts from flaking the release gate#114

Merged
VickyXAI merged 1 commit into
mainfrom
fix/flaky-cli-spawn-timeouts
Jul 21, 2026
Merged

test: stop CLI-spawn timeouts from flaking the release gate#114
VickyXAI merged 1 commit into
mainfrom
fix/flaky-cli-spawn-timeouts

Conversation

@VickyXAI

Copy link
Copy Markdown
Contributor

Four tests failed intermittently under parallel load while passing in ~0.6s idle. Same cause in all four: a spawn bound sized for an unloaded machine.

test observed bound
cli: franklin task tail reconciles stale queued tasks 5045ms 5000
cli: franklin task cancel <runId> kills running task 5841ms 5000
cli: franklin task wait reconciles stale queued tasks 5048ms 5000
franklin skills --json emits a parseable structure 8037ms 8000

Those durations are the bound firing, not the work. The same tests run in ~640ms idle. node dist/index.js boots the entire CLI — config load, MCP discovery, wallet init — before the subcommand executes, and that startup balloons under concurrency.

Three things were wrong beyond the number

1. The failures were undiagnosable. On timeout spawnSync returns status: null with empty stderr, so assert.equal(result.status, 0, result.stderr.toString()) printed null !== 0 and named nothing. The first time I hit this I could not tell what had failed and nearly dismissed it. assertCliExit() now says the bound was exceeded and that a loaded machine is the likely cause, not broken code.

2. skills.local.mjs had two bounds fighting. runCli defaulted to timeoutMs = 8000 while the tests around it declare { timeout: 15_000 } — the inner bound always won, so the declared one never applied. Two call sites had already been patched to timeoutMs: 15_000 individually, which is the usual sign that the default is what's wrong, not the call site.

3. One test had its bounds inverted. task wait ... --timeout 10000 ran under a 5000ms spawn bound, so the harness would have killed the process before the behaviour under test could finish. It only passed because the task goes lost almost immediately; a slower reconcile would have failed for reasons having nothing to do with task wait.

The fix

Both files use CLI_SPAWN_TIMEOUT_MS = 20_000, matching what test/local.mjs already used for its other CLI-spawning tests ({ timeout: 20_000 } at lines 117, 127, 597, …). 5s and 8s were the outliers, not the new value.

Why this matters more than four slow tests

A flaky test in a release gate is worse than a slow one — it trains you to wave the next real failure past as "just that one again". This session hit exactly that: a suite run went red mid-release, and the honest options were to investigate or to shrug. Had I shrugged, the flake would have been indistinguishable from a genuine regression.

Verification

Not just a clean run — two full suites executed concurrently, which is the condition that produced the original failures. 636 pass on both. Also 3 sequential runs, all green.

Four tests failed intermittently under parallel load while passing in ~0.6s
idle. All the same cause: a spawn bound sized for an unloaded machine.

  cli: franklin task tail reconciles stale queued tasks   5045ms  (bound 5000)
  cli: franklin task cancel <runId> kills running task    5841ms  (bound 5000)
  cli: franklin task wait reconciles stale queued tasks   5048ms  (bound 5000)
  franklin skills --json emits a parseable structure      8037ms  (bound 8000)

The durations were the bound firing, not the work taking that long — the same
tests run in ~640ms when the machine is idle. `node dist/index.js` boots the
whole CLI (config load, MCP discovery, wallet init) before the subcommand runs,
and that startup balloons under concurrency.

Three things were wrong beyond the number:

1. The failures were undiagnosable. On timeout spawnSync returns status:null
   with empty stderr, so `assert.equal(result.status, 0, result.stderr)` printed
   "null !== 0" and named nothing. First time I hit this I could not tell what
   had failed. assertCliExit() now says it exceeded the bound and that a loaded
   machine is the likely cause.

2. skills.local.mjs had two bounds fighting: runCli defaulted to 8000ms while
   the tests around it declared { timeout: 15_000 }, so the inner one always
   won and the declared one never applied. Two call sites had already been
   patched to timeoutMs: 15_000 individually — the usual sign that the default,
   not the call site, is what's wrong.

3. One test told the CLI `--timeout 10000` under a 5000ms spawn bound, so the
   harness would have killed the process before the behaviour under test could
   finish. It only passed because the task goes 'lost' almost immediately.

Both files now use CLI_SPAWN_TIMEOUT_MS = 20_000, matching what test/local.mjs
already used for its other CLI-spawning tests ({ timeout: 20_000 }); 5s and 8s
were the outliers.

A flaky test in a release gate is worse than a slow one: it teaches you to wave
the next real failure past as "just that one again". Verified with two suites
running concurrently — the condition that produced the original failures —
636 pass on both.
@VickyXAI
VickyXAI merged commit fd5abed into main Jul 21, 2026
2 checks passed
@VickyXAI
VickyXAI deleted the fix/flaky-cli-spawn-timeouts branch July 21, 2026 05:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant